Description:
安裝 nodemon
到這裡決定先安裝 nodemon,透過nodemon 啟動 Server 的話,只要 js 有更動,就會自動重啟
雖然講師重啟得很開心,但我有點懶得一直手動重啟
註:jade & css 除外,改這兩個不需要重啟Server,nodemon 也不會偵測到這個改動
使用 npm install 安裝
加上 --save-dev option 代表這個 dependencies 是給開發人員使用的,也會自動加到package.jsonnpm install --save-dev nodemon
安裝完打開package.json,在 scripts 區塊加上執行 nodemon 指令: "dev"
...
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www"
},
...
使用以下指定啟動,之後程式變更時就會自動重啟 Servernpm run dev
App & Module Setup
安裝express-generator globallynpm install -g express-generator
透過express建立新project目錄 4_nodeblogexpress 4_nodeblog
修改package.json,加入要用的 dependencies
{
"name": "4-nodeblog",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www"
},
"dependencies": {
"body-parser": "~1.16.0",
"cookie-parser": "~1.4.3",
"debug": "~2.6.0",
"express": "~4.14.1",
"jade": "~1.11.0",
"morgan": "~1.7.0",
"serve-favicon": "~2.3.2",
"monk": "https://github.com/vccabral/monk.git",
"connect-flash": "*",
"express-session": "*",
"express-validator": "*",
"express-messages": "*",
"multer": "*",
"moment": "*",
"mongodb": "*"
},
"devDependencies": {
"nodemon": "^1.11.0"
}
}
monk: 類似於mongoose,MongoDB ORM,這邊用monk是想提供多種練習,再去選擇自己喜歡哪一種
moment: javascript library,用來format 日期時間格式
其他 module 都和 nodeauth project 類似,就不再多說明
安裝 modulesnpm install
修改 app.js,import module
var session = require('session');
var multer = require('multer');
var upload = multer({ dest: './public/images' })
var expressValidator = require('express-validator');
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');
app.locals.moment = require('moment');
routing,讓 router 可以存取到 DB
// Make our db accessible to our router
app.use(function(req, res, next){
req.db = db;
next();
});
加入 connect-flash, validator, session middleware (從 project 3 copy過來)
// Connect-Flash
app.use(require('connect-flash')());
app.use(function (req, res, next) {
res.locals.messages = require('express-messages')(req, res);
next();
});
// validator
app.use(expressValidator({
errorFormatter: function(param, msg, value) {
var namespace = param.split('.')
, root = namespace.shift()
, formParam = root;
while(namespace.length) {
formParam += '[' + namespace.shift() + ']';
}
return {
param : formParam,
msg : msg,
value : value
};
}
}));
// Handle Sessions
app.use(session({
secret:'secret',
saveUninitialized: true,
resave: true
}));
Layout template
這次不使用bootstrap,只使用jade
layout.jade
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
.container
img.logo(src='/images/nodebloglogo.png')
nav
ul
li
a(href='/') Home
li
a(href='/posts/add') Add Post
li
a(href='/categories/add') Add Category
block content
footer
p NodeBlog © 2017
這邊會需要一張 logo 圖,可以隨意放自己喜歡的圖,或是到這個免費建logo的網址做一個
把圖片放到 project 下的 public\images 中,檔名需與 layout.jade 中定義的一致 (nodebloglogo.png)
修改style.css,撰寫css樣式
body {
font: 15px Helvetica, Arial, sans-serif;
background: #f4f4f4;
color: #666;
}
.logo {
text-align: center;
margin: auto;
padding-bottom: 10px;
display: block;
}
.container {
width: 750px;
border: 1px solid #ccc;
margin: 20px auto;
padding: 20px;
border-top: #83cd39 3px solid;
}
.clr {
clear: both;
}
ul {
padding: 0;
margin: 0;
}
h1,h2,h3,p {
padding: 5px 0;
margin-bottom: 0;
}
p {
margin: 0;
}
a {
color: #00B7FF;
}
nav {
background: #404137;
overflow: auto;
height: 40px;
padding: 20px 0 0 10px;
font-size: 10px;
}
nav li {
float: left;
list-style: none;
}
nav a {
padding: 10px;
margin: 0 10px;
color: #fff;
}
nav a.current, nav a:hover {
background: #83cd29;
color: #000;
}
除了圖片之外,其他樣式應該如下圖
上面的配色可以隨意調配,分享兩個之前我自己有在用的網站:
Color Drop 這個網站提供多種顏色並排的比較,可以用來查看網站多種色彩的搭配效果
Paletton 也提供網站色彩搭配,而且附有一個大大的調色盤
首頁顯示貼文
在 Mongo Shell create nodeblog DB,並新增 categories 和 posts 兩個 collection
use nodeblog
db.createCollection('categories');
db.createCollection('posts');
新增兩筆資料,等下測試要用
db.posts.insert({title:"Blog Post One", category:"Technology", arthor:"yuki", body:"This is the bo dy", date:ISODate()});
db.posts.insert({title:"Blog Post Two", category:"Science", arthor:"grace", body:"This is the body ", date:ISODate()});
query posts collection,確認資料有塞進去db.posts.find().pretty();
修改 routes\index.js,加入 mongo db module 及 HTTP GET request
var express = require('express');
var router = express.Router();
//mongo db
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');
/* GET home page. */
router.get('/', function(req, res, next) {
var db = req.db;
var posts = db.get('posts');
posts.find({}, {}, function(err, posts){
res.render('index', { posts: posts });
});
});
module.exports = router;
修改 index.jade,如果有任何posts,把每個post列出來
title含有超連結,利用post的 _id 作為routing path
extends layout
block content
if posts
each posts, i in posts
.post
h1
a(href='/posts/show/#{post._id}')
=post.title
修改 style.css
美化幾個地方
/* 分類 */
.meta{
padding: 7px;
border: 1px solid #ccc;
background: #ccc;
margin-bottom: 10px;
}
/* Read More 連結 */
a.more{
display: block;
width: 80px;
background: #404137;
color: #fff;
padding: 10px;
margin-top: 30px;
text-decoration: none;
}
/* 貼文 */
.post{
border-bottom: 1px solid #ccc;
padding-bottom: 20px;
}
/* 貼文Title連結 */
.post h1 a{
color: #666;
text-decoration: none;
}
使用 Moment 加上貼文的分類、作者、日期和貼文內容,最後再加上 Read More 連結
extends layout
block content
if posts
each post, i in posts
.post
h1
a(href='/posts/show/#{post._id}')
=post.title
p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
=post.body
a.more(href='/posts/show/#{post._id}') Read More
弄完之後應該長得像這樣
新增貼文功能
接下來要撰寫新增貼文的功能
修改 app.js 的 routing,把 users 改成 posts
...
var index = require('./routes/index');
var posts = require('./routes/posts');
...
app.use('/', index);
app.use('/posts', posts);
...
在 routes 下新增 posts.js,內容從 user.js copy過來,把user.js刪掉(這邊用不到)
修改成下面這樣:
var express = require('express');
var router = express.Router();
router.get('/add', function(req, res, next) {
res.render('addpost', {
'title': 'Add Post'
});
});
module.exports = router;
接下來要為 posts 新增 view
在 view 下新增 addpost.jade,撰寫template
extends layout
extends layout
block content
h1=title
ul.errors
if errors
each error, i in errors
li.alert.alert-danger #{error.msg}
form(method='post', action='/posts/add', enctype="multipart/form-data")
.form-group
label Title:
input.form-control(name='title', type='text')
.form-group
label Category:
select.form-control(name='category')
.form-group
label Body:
textarea.form-control(name='body', id='body')
.form-group
label Main Image:
input.form-control(name='mainimage', type='file')
.form-group
label Author:
select.form-control(name='author')
option(value='byakuinss') byakuinss
option(value='yuki') yuki
input.btn.btn-default(name='submit', type='submit', value='Save')
html 寫完了,再來修改 style.css,為 addpost.jade 加上css樣式
input, select, textarea{
margin-bottom: 15px;
}
label{
display: inline-block;
width: 180px;
}
input[type='text'], select, textarea{
padding: 3px;
height: 20px;
width: 200px;
border: 1px #ccc solid;
}
select{
height: 28px;
}
textarea{
height: 70px;
width: 400px;
}
接下來要將貼文存到DB,需要在 posts.js 加入 HTTP POST request
POST request 主要有以下幾個動作
//Require multer to handle image
var multer = require('multer');
var upload = multer({ dest: './public/images' });
......
router.post('/add', upload.single('mainimage'), function(req, res, next) {
//Get Form Values
var title = req.body.title;
var category = req.body.category;
var body = req.body.body;
var author = req.body.author;
var date = new Date();
//Check Image Upload
if(req.file){
var mainimage = req.file.filename;
} else {
var mainimage = 'no-image.jpg'
}
//Form Validation
req.checkBody('title', 'Title field is required').notEmpty();
req.checkBody('body', 'Body field is required').notEmpty();
//Check Errors
var errors = req.validationErrors();
if(errors){
res.render('addpost', {
"errors": errors
});
} else {
var posts = db.get('posts');
posts.insert({
"title": title,
"body": body,
"category": category,
"date": date,
"author": author,
"mainimage": mainimage
}, function(err, post){
if(err) {
res.send(err);
} else {
req.flash('success', 'Post Added');
res.location('/');
res.redirect('/');
}
});
}
});
......
重啟 Server,試著新增一篇貼文,此時的 Category 沒有資料,這項先跳過不選
新增完應該會看到剛剛新增的貼文出現在首頁
現在來補足 Category 選單,先到 Mongo Shell在 categories collection 手動新增幾筆資料
db.categories.insert({name:'Technology'});
db.categories.insert({name:'Science'});
db.categories.insert({name:'Business'});
要讓 Category 選單從 categories collection 抓出資料,需要在 GET Add Post 頁面時加入DB連線,並將資料存到 categories
修改 posts.js 的 GET request 內容,將取得的DB資料存入 categories 參數
router.get('/add', function(req, res, next) {
var categories = db.get('categories');
categories.find({}, {}, function(err, categories){
res.render('addpost', {
'title': 'Add Post',
'categories': categories
});
});
});
接下來要從 categories 參數中取出值,並串連到 addpost.jade 的 category 選單
修改 addpost.jade,在 Category 選單下加入兩行,針對每一個 category,在選單中顯示
category.name
...
.form-group
label Category:
select.form-control(name='category')
each category, i in categories
option(value='#{category.name}') #{category.name}
...
在選單中可以看到所有 categories 了
新增一篇帶有 category 的貼文,測試成功
文字編輯器
在新增貼文時,如果有文字編輯器,貼文的內容就可以有更多變化
這邊選擇的是 CKEditor,因為比較容易Setup
到CKEditor官網下載 Standard Package
下載後解壓會產生ckeditor folder,把整個 folder 複製到 project folder 的 public 資料夾下
修改 addpost.jade,在最下方加入script,import ckeditor.js,並且用來取代原本 body 區塊
...
input.btn.btn-default(name='submit', type='submit', value='Save')
script(src='/ckeditor/ckeditor.js')
script
| CKEDITOR.replace('body');
...
重新進入 Add Post 頁面,就會看到原本 body 區塊的 textarea 已經變成文字編輯器囉
新增Category功能
修改 app.js,加入 categories routing
...
var index = require('./routes/index');
var posts = require('./routes/posts');
var categories = require('./routes/categories')
...
app.use('/', index);
app.use('/posts', posts);
app.use('/categories', categories);
...
新增兩個檔案: addcategory.jade \ categories.js
複製 addpost.jade 到 addcategory.jade,只留下一個 text 和 button,如下
extends layout
block content
h1=title
ul.errors
if errors
each error, i in errors
li.alert.alert-danger #{error.msg}
form(method='post', action='/categories/add')
.form-group
label Name:
input.form-control(name='name', type='text')
input.btn.btn-default(name='submit', type='submit', value='Save')
同樣複製 posts.js 到 categories.js,只留下需要的 module,修改 GET \ POST request 內容
var express = require('express');
var router = express.Router();
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');
router.get('/add', function(req, res, next) {
res.render('addcategory', {
'title': 'Add Category'
});
});
router.post('/add', function(req, res, next) {
//Get Form Values
var name = req.body.name;
//Form Validation
req.checkBody('name', 'Name field is required').notEmpty();
//Check Errors
var errors = req.validationErrors();
if(errors){
res.render('addcategories', {
"errors": errors
});
} else {
var categories = db.get('categories');
categories.insert({
"name": name
}, function(err, category){
if(err) {
res.send(err);
} else {
req.flash('success', 'Category Added');
res.location('/');
res.redirect('/');
}
});
}
});
module.exports = router;
新增一個 Category 測試,新增完再到 Add Post,新category已經出現在選單中了
縮短文字內容 (truncate text) & 顯示上傳圖片
把之前測試的新增貼文都刪除db.posts.remove("");
修改layout.jade,加入success message
...
li
a(href='/categories/add') Add Category
!= messages()
block content
...
修改style.css,加入 success message 顯示的css樣式
...
ul.success li{
padding: 15px;
margin-top: 10px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
border-color: #d6e9c6;
list-style: none;
}
接下來加入truncate text效果
新增一篇很長的貼文
修改app.js,加入一個新function: truncateText
...
app.locals.moment = require('moment');
app.locals.truncateText = function(text, length){
var truncateText = text.substring(0, length);
return truncateText;
}
...
修改index.jade,將=post.body改成 !=truncateText(post.body,400)
......
p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
!=truncateText(post.body,400)
a.more(href='/posts/show/#{post._id}')
......
重啟server,可以看到過長的貼文被截掉了
接下來要把 image 加入貼文
先確認新增貼文時上傳的圖片有在 public\images中
修改 index.jade,在 p.meta 下方加入圖片label (可以依自己喜好隨意放)
...
p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
img(src='/images/#{post.mainimage}')
!=truncateText(post.body,400)
...
修改 style.css加上圖片css樣式,因為上傳的圖片可能大小不一,我希望圖片都是隨視窗改變大小
.post img {
width: 100%;
}
重新整理網頁,圖片就出來囉
註:沒看到圖片怎麼辦
以Category View檢視貼文
需要新增一個新頁面,用Category當作query條件show出對應的貼文
修改categories.js,加入新的 route,將要query的category條件放入 posts.find({query_condition}, {}, function ...)
router.get('/show/:category', function(req, res, next) {
var posts = db.get('posts');
posts.find({category: req.params.category}, {}, function(err, posts){
res.render('index', { //切換回index以顯示貼文
'title': req.params.category, //標題為query的category
'posts': posts //顯示query到的貼文
});
});
});
修改index.jade,將category文字改成連結,點選連結就會顯示出該分類的所有貼文
....
p.meta Posted in
a(href='/categories/show/#{post.category}') #{post.category}
by #{post.author}
on #{moment(post.date).format("MM-DD-YYYY")}
img(src='/images/#{post.mainimage}')
...
重啟 Server 測試,看到 Category 文字帶有連結,且點選後只出現該分類的貼文
檢視單篇貼文內容
目前點選 Read More 按鈕,還無法顯示單篇貼文內容
和分類顯示相同,需要為單篇貼文顯示建立新的 route
修改 posts.js,透過 id 找到對應的貼文並以 show view 顯示
...
router.get('/show/:id', function(req, res, next) {
var posts = db.get('posts');
posts.findById(req.params.id, function(err, post){
res.render('show', {
'post': post
});
});
});
...
新增 views\show.jade 檔案,內容從 index.jade copy 過來修改
因為show只需要顯示單篇貼文,所以去掉 loop, title 連結,truncate text改成顯示完整body
extends layout
block content
.post
h1=post.title
p.meta Posted in
a(href='/categories/show/#{post.category}') #{post.category}
by #{post.author}
on #{moment(post.date).format("MM-DD-YYYY")}
img(src='/images/#{post.mainimage}')
!=post.body
重啟 Server,點選 Read More 就可以顯示完整的單篇貼文了
在單篇貼文中加入 comments 區塊
修改 show.js,從 post.body 往下新增 comments 區塊
如果目前有任何 comments就會顯示,無論是否有已存在 comments 都會顯示新增comment的表單
...
img(src='/images/#{post.mainimage}')
!=post.body
br
hr
if post.comments
h3 Comments
each comment, i in post.comments
.comment
p.comment-name #{comment.name}
p.comment-body #{comment.body}
br
h3 Add Comment
if errors
ul.errors
each error, i in errors
li.alert.alert-danger #{error.msg}
form.comment-form(method='post', action='/posts/addcomment')
input(name='postid', type='hidden', value='#{post._id}')
.form-group
label Name
input.form-control(type='text', name='name')
.form-group
label Email
input.form-control(type='text', name='email')
.form-group
label Body
textarea.form-control(type='text', name='body')
br
input.btn.btn-default(type='submit', name='submit', value='Add Comment')
接下來為 Add Comment 按鈕撰寫 POST request
修改 post.js,加入新的 POST route (從 add post copy)
router.post('/addcomment', function(req, res, next) { //修改名稱為addcomment
//Get Form Values
var name = req.body.name;
var email = req.body.email;
var body = req.body.body;
var postid = req.body.postid;
var commentdate = new Date();
//Form Validation
req.checkBody('name', 'Name field is required').notEmpty();
req.checkBody('email', 'Email field is required but never displayed').notEmpty();
req.checkBody('email', 'Email field is not formatted properly').isEmail();
req.checkBody('body', 'Body field is required').notEmpty();
//Check Errors
var errors = req.validationErrors();
if(errors){ //如果add comment有error仍要顯示貼文
var posts = db.get('posts');
posts.findById(postid, function(err, post){
res.render('show', {
"errors": errors,
"post": post
});
});
} else { //如果沒有錯誤,將comment內容update到該篇貼文的comments欄位
var comment = {
"name": name,
"email": email,
"body": body,
"commentdate": commentdate
}
var posts = db.get('posts');
posts.update({
"_id": postid
}, {
$push: {
"comments": comment
}
}, function(err, doc){ //都沒有問題就切換到單篇貼文頁面
if(err){
throw err;
} else {
req.flash('success', 'Comment Added');
res.location('/posts/show/'+postid);
res.redirect('/posts/show/'+postid);
}
});
}
});
重啟 Server,新增一篇 comment 測試看看,網頁會自動切換,也會顯示成功新增comment的訊息